package com.minibobbo.utilities.animobject;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.Fixture;
import com.badlogic.gdx.utils.Array;
import com.minibobbo.utilities.extrectangle.eRectangle;

/**
 * This is an animation object. You use this object in multiple steps: Step 1 -
 * Store an array of texture regions that will be played for all the different
 * animations this animation object can use. Step 2 - Assign labels to sets of
 * texture regions that define a single iteration of the animation, define how
 * long it should play. Step 3 (optional) - assign the animation object to a
 * rectangle or a box2D body so it can be drawn in the correct location
 * automatically.
 * 
 * @author MiniBobbo
 * 
 */
public class AnimObject {

	public enum AnchorPoint {
		TOP_CENTER, BOTTOM_CENTER, CENTER, LEFT_CENTER, RIGHT_CENTER, TOP_LEFT, BOTTOM_LEFT, TOP_RIGHT, BOTTOM_RIGHT

	}

	// Texture to hold spritesheet
	private TextureRegion[] tr;

	private AnchorPoint anchor = AnchorPoint.CENTER;
	private eRectangle drawSize;

	// Boolean that holds if the picture is facing right.
	private boolean facingRight = true;

	// Array of animation labels
	private Array<AnimLabel> labels;

	// This is the label that is currently active
	private AnimLabel activeLabel;

	// An array of all the labels that should play. They play one after another
	// until there is only one left. If there is more than one label in the
	// queue the animation is active. If there is only one left it plays on a
	// loop and is passive.
	private Array<AnimLabel> labelQueue;

	// Should the animation be visible? Normally this is used to keep the
	// animation from displaying until the delay runs out.
	private boolean visible = true;

	// Box2D body that this animation object is attached to.
	private Body b = null;

	// The Box2D fixture this animation object is attached to.
	private Fixture fix = null;
	private float width, height;

	// A rectangle that this animation object is attached to.
	public eRectangle r = null;

	// A sprite that we will use to draw an attached animation object
	private Sprite s;

	// If the animation should be changed to a passive animation this time
	// around.
	public boolean nowPassive = false;

	// Should this animation be disposed the next time the animation manager
	// updates?
	public boolean disposeMe = false;

	/**
	 * These are the animation variables. We keep track of how long the total
	 * animation is and how long we should be playing it for.
	 */
	private float elapsedTime;
	private int currentFrame;

	/**
	 * These are the flashing variables.
	 */
	private Color flashColor;
	private float flashLength;
	private float elapsedFlash;

	/**
	 * Creates and initializes the animation
	 */
	public AnimObject() {
		init();

	}

	/**
	 * Initializes all the variables
	 */
	public void init() {
		labels = new Array<AnimLabel>();
		labelQueue = new Array<AnimLabel>();
		s = new Sprite();
		drawSize = new eRectangle();
	}

	/**
	 * Sets the supplied texture regions as the frames that should be animated.
	 * Creates a default label that includes all textures that runs on a 5
	 * second loop. Also creates a "Dispose" label that will dispose the
	 * animation.
	 * 
	 * @param tr
	 *            Texture Regions to use as frames.
	 */
	public void loadTextureRegions(TextureRegion[] tr) {
		this.tr = tr;
		// Creates a default label and sets it as current.
		labels.add(new AnimLabel("default", 0, tr.length - 1, 5.0f));
		labels.add(new AnimLabel("Dispose", 0, 1, 1));

		// setLabel("default");

		this.setLabel("default");
		currentFrame = 0;

	}

	public void splitTexture(Texture t, int width, int height) {
		TextureRegion[][] startTex;
		TextureRegion[] endTex;

		startTex = Sprite.split(t, width, height);
		int x, y;
		y = startTex.length;
		x = startTex[0].length;

		endTex = new TextureRegion[startTex.length * startTex[0].length];

		// Loop through the multidimensional array and make it single
		// dimensional.
		for (int down = 0; down < y; down++) {
			for (int right = 0; right < x; right++) {
				// If we haven't looped through all the textures yet.
				if ((down * x) + right < endTex.length) {
					endTex[(down * x) + right] = new TextureRegion(
							startTex[down][right]);
				}
			}
		}

		loadTextureRegions(endTex);

	}

	/**
	 * Draws the scaled animation at the specified location. Used for detached
	 * animation objects.
	 * 
	 * @param sb
	 *            Sprite batch used to draw
	 * @param x
	 *            X location (lower left)
	 * @param y
	 *            Y location (lower left)
	 * @param width
	 *            Width of the animation
	 * @param height
	 *            Height of the animation
	 */
	public void draw(SpriteBatch sb, float x, float y, float width, float height) {
		if (visible)
			sb.draw(this.getCurrentFrame(), x, y, width, height);
	}

	/**
	 * Draws the animation at the specified location with no resizing. Used for
	 * detached animation objects.
	 * 
	 * @param sb
	 *            Sprite batch used to draw
	 * @param x
	 *            X location (lower left)
	 * @param y
	 *            Y location (lower left)
	 */
	public void draw(SpriteBatch sb, float x, float y) {
		if (visible)
			sb.draw(this.getCurrentFrame(), x, y);
	}

	/**
	 * Draws the ATTACHED animation object in the location and size of the
	 * object it is attached to.
	 * 
	 * @param sb
	 *            Sprite batch for drawing
	 */
	public void draw(SpriteBatch sb) {
		if (visible) {
			if (fix != null) {
				s.setRegion(this.getCurrentFrame());
				s.setBounds(b.getLocalCenter().x
						- getCurrentFrame().getRegionWidth() / 2,
						b.getLocalCenter().y
								- getCurrentFrame().getRegionWidth() / 2,
						width, height);
				s.setOrigin(s.getWidth() / 2, s.getHeight() / 2);
				s.setRotation(b.getAngle() * MathUtils.radiansToDegrees - 90);
				s.draw(sb);
			}
			if (r != null) {

				s.setRegion(this.getCurrentFrame());
				// How do we scale this?
				if (anchor == AnchorPoint.CENTER)
				s.setBounds(r.x - ((drawSize.width - r.width) / 2), r.y
						- ((drawSize.height - r.height) / 2), drawSize.width,
						drawSize.height);
				if (anchor == AnchorPoint.BOTTOM_CENTER)
					s.setBounds(r.x - ((drawSize.width - r.width) / 2), r.y,
							drawSize.width, drawSize.height);
				if (anchor == AnchorPoint.BOTTOM_LEFT)
					s.setBounds(r.x, r.y, drawSize.width, drawSize.height);
				if (anchor == AnchorPoint.BOTTOM_RIGHT)
					s.setBounds(r.getRightSide() - drawSize.width, r.y,
							drawSize.width, drawSize.height);
				if (anchor == AnchorPoint.LEFT_CENTER)
					s.setBounds(r.x, r.y - ((drawSize.height - r.height) / 2),
							drawSize.width, drawSize.height);
				if (anchor == AnchorPoint.RIGHT_CENTER)
					s.setBounds(r.x - drawSize.width, r.y
							- ((drawSize.height - r.height) / 2),
							drawSize.width, drawSize.height);
				if (anchor == AnchorPoint.TOP_CENTER)
					
				s.setBounds(r.x - ((drawSize.width - r.width) / 2),
						r.getTopSide() - drawSize.height, drawSize.width,
						drawSize.height);
				if (anchor == AnchorPoint.TOP_LEFT)
					
				s.setBounds(r.x, r.getTopSide() - drawSize.height,
						drawSize.width, drawSize.height);
				if (anchor == AnchorPoint.TOP_RIGHT)
					
				s.setBounds(r.x - (drawSize.width - r.width), r.getTopSide()
						- drawSize.height, drawSize.width, drawSize.height);

				s.setOrigin(s.getWidth() / 2, s.getHeight() / 2);
				s.draw(sb);
			}
		}
	}

	/**
	 * Is this animation object attached to a Box2D body or a rectangle? If not,
	 * then we have to use draw methods that specify locations.
	 * 
	 * @return True if yes, otherwise false
	 */
	public boolean isAttached() {
		if (b != null || r != null)
			return true;
		else
			return false;
	}

	/**
	 * The update method takes the time elapsed and updates what frame should be
	 * showing.
	 */
	public void update() {
		// Update the elapsed time and the queue time.
		elapsedTime += Gdx.graphics.getDeltaTime();

		if (elapsedTime >= activeLabel.timePerFrame())
			// Advance to the next frame. If we past the last frame, go to the
			// next in the queue, unless there isn't one. In which case, loop
			// it.
			if (this.nextFrame()) {
				// If we have more than one label in queue remove it and set the
				// next as active.
				if (labelQueue.size > 1) {
					labelQueue.removeIndex(0);
					setActiveLabel(labelQueue.first());
				}
			}

		if (flashColor != null) {
			elapsedFlash += elapsedTime;
			// If it has been longer than our duration, return the sprite to
			// normal and clear the flashing variable.
			if (elapsedFlash >= flashLength) {
				s.setColor(Color.WHITE);
				flashColor = null;
			}
		}

	}

	/**
	 * Makes the object flash a colorfor a set amount of time.
	 * 
	 * @param flash
	 *            The color the object should flash
	 * @param duration
	 *            How long should the object flash.
	 */
	public void objectFlash(Color flash, float duration) {
		s.setColor(flash);
		flashColor = flash;
		flashLength = duration;
		elapsedFlash = 0;
	}

	/**
	 * If anything needs to be disposed of, this is the place to do it.
	 */
	public void dispose() {
		labels.clear();
		labelQueue.clear();

	}

	/**
	 * Creates a new label
	 * 
	 * @param label
	 *            Name of the label
	 * @param startFrame
	 *            Frame the label starts on
	 * @param endFrame
	 *            Frame the label ends on
	 * @param duration
	 *            How long in milliseconds the label should play in seconds.
	 * @return True if the label was added. False if it already existed.
	 */
	public boolean addLabel(String label, int startFrame, int endFrame,
			float duration) {
		boolean found = false;
		for (int i = 0; i < labels.size; i++) {
			if (labels.get(i).name == label) {
				found = true;
			}
		}
		if (!found) {
			labels.add(new AnimLabel(label, startFrame, endFrame, duration));
		}
		return !found;
	}

	/**
	 * Attaches this animation object to a Box2D body. If set, then the draw
	 * method can be called without supplying a location and the animation will
	 * be drawn where the body currently is, scaled to fit.
	 * 
	 * @param b
	 *            The Box2D body that this animation object use for a draw
	 *            location.
	 * @param width
	 *            The width of the animation object
	 * @param height
	 *            The height of the animation object
	 */
	public void setBox2DBody(Body b, float width, float height) {
		this.b = b;
		this.width = width;
		this.height = height;
	}

	/**
	 * Attaches the animation object to a Rectangle. If set, the draw method can
	 * be called without supplying a location and the animation will be drawn at
	 * the rectangle's location scaled to fit.
	 * 
	 * @param r
	 *            Rectangle this animation object should be attached to.
	 */
	public void setRectangle(Rectangle r) {
		this.r = (eRectangle) r;
		drawSize.set(r.x, r.y, r.width, r.height);
		Gdx.app.log("drawSize rect height: ", "" + drawSize.height);
	}

	/**
	 * Allows us to specify a different draw size than the rectangle the
	 * animation object is attached to. NOTE: IF THIS ANIM OBJECT IS NOT
	 * ATTACHED TO A RECTANGLE THIS METHOD DOES NOTHING.
	 * 
	 * @param width
	 *            Width to draw
	 * @param height
	 *            Height to draw
	 */
	public void setDrawSize(float width, float height) {
		drawSize.width = width;
		drawSize.height = height;
	}

	/**
	 * Sets the anchor point for the animation object as attached to the
	 * rectangle. If the draw size is different from the attached rectangle size
	 * the image will scale from this point. The default is center.
	 * 
	 * @param anchorPoint
	 *            The point the image should be scaled from.
	 */
	public void setAnchorPoint(AnchorPoint anchorPoint) {
		anchor = anchorPoint;
	}

	/**
	 * Advances to the next frame. If this is the last frame, return false;
	 * 
	 * @return True if we looped. False otherwise;
	 */
	private boolean nextFrame() {
		elapsedTime = 0;
		currentFrame++;

		if (currentFrame >= activeLabel.endFrame) {
			currentFrame = activeLabel.startFrame;
			return true;

		} else
			return false;
	}

	/**
	 * Tries to set this animation object's active animation. Clears the
	 * animation queue, so this is the only one.
	 * 
	 * @param label
	 *            The animation label that this animation object should attempt
	 *            to use. It also sets the frameTime variable.
	 * @return True if it was able to set to the supplied label. False
	 *         otherwise.
	 */
	public boolean setLabel(String label) {
		boolean found = false;

		if (label == "Dispose") {
			visible = false;
			disposeMe = true;
		} else {
			for (int i = 0; i < labels.size; i++) {
				if (labels.get(i).name == label) {
					labelQueue.clear();
					labelQueue.add(labels.get(i));
					activeLabel = labels.get(i);
					found = true;
					currentFrame = activeLabel.startFrame;
				}
			}

		}
		return found;
	}

	private void setActiveLabel(AnimLabel a) {
		activeLabel = a;
		currentFrame = activeLabel.startFrame;

	}

	/**
	 * Queues an animation label up to play. The label lasts only as long as the
	 * duration.
	 * 
	 * @param label
	 *            What label should we play?
	 * @return Did we find the label?
	 */
	public boolean queueAnimation(String label) {
		nowPassive = false;
		AnimLabel temp;
		temp = isLabelPresent(label);
		// If we weren't able to find a label, return false.
		if (temp == null)
			return false;

		// Add the new label to the label queue.
		labelQueue.add(temp);
		return true;

	}

	/**
	 * Gets the texture region of the active frame and returns it so the
	 * animation manager can draw it.
	 * 
	 * @return The Texture Region of the active frame.
	 */
	public TextureRegion getCurrentFrame() {
		if (tr[currentFrame].isFlipX() && facingRight)
			tr[currentFrame].flip(true, false);
		else if (!tr[currentFrame].isFlipX() && !facingRight)
			tr[currentFrame].flip(true, false);

		return tr[currentFrame];

	}

	/**
	 * Gets a texture region from in the middle of the animation
	 * 
	 * @param i
	 *            The frame number you want to retrieve
	 * @return The texture region of the image
	 */
	public TextureRegion getFrame(int i) {
		return tr[i];
	}

	/**
	 * Gets the current label's duration. Used for queueing up future
	 * animations.
	 * 
	 * @return The amount current label's duration.
	 */
	public float getActiveLabelDuration() {
		return activeLabel.getDuration();
	}

	/**
	 * Gets the specified label's duration. Used for queueing up future
	 * animations.
	 * 
	 * @param label
	 *            the label that you need the duration of.
	 * @return The duration of the label.
	 */
	public float getLabelDuration(String label) {
		for (int i = 0; i < labels.size; i++) {
			if (labels.get(i).name == label)
				return labels.get(i).getDuration();
		}
		return 0;
	}

	/**
	 * Searches through all the labels for the animation and looks for the one
	 * specified.
	 * 
	 * @param label
	 *            Label to search for.
	 * @return The label we found.
	 */
	public AnimLabel isLabelPresent(String label) {
		for (int i = 0; i < labels.size; i++) {
			if (labels.get(i).getName() == label)
				return labels.get(i);

		}
		Gdx.app.log("Couldn't find label " + label, "Not present.");
		return null;
	}

	public boolean isVisible() {
		return visible;
	}

	public void setVisible(boolean visible) {
		this.visible = visible;
	}

	/**
	 * Flips the image so it is facing the other way.
	 */
	public void flipMe() {
		facingRight = !facingRight;

	}

	/**
	 * Gets all the texture regions. Only used for testing.
	 * 
	 * @return Texture Region array that this animation has.
	 */
	public TextureRegion[] getRegions() {
		return tr;

	}

	/**
	 * Gets the width of the current Texture Area
	 * 
	 * @return Width of the current Texture area
	 */
	public float getWidth() {
		return getCurrentFrame().getRegionWidth();
	}

	/**
	 * Gets the height of the current texture area
	 * 
	 * @return The height of the current texture area.
	 */
	public float getHeight() {

		return getCurrentFrame().getRegionHeight();
	}

	/**
	 * Animation label used internally. Holds the label name, start frame, end
	 * frame and how long it should take for the animation to play.
	 * 
	 * @author Dagobah
	 * 
	 */
	private class AnimLabel {
		// Name of the animation
		private String name;

		// Start and end frame
		private int startFrame;
		private int endFrame;

		// Time it takes for the animation to play in MS.
		private float duration;

		/**
		 * Creates an animation label. Default is an active label.
		 * 
		 * @param name
		 *            name of label
		 * @param startFrame
		 *            what frame it starts on
		 * @param endFrame
		 *            what frame it ends on. If this is -1, this is a single
		 *            frame picture and not an animation.
		 * @param duration
		 *            how long does it take in seconds
		 */
		public AnimLabel(String name, int startFrame, int endFrame,
				float duration) {
			this.name = name;
			this.startFrame = startFrame;
			this.endFrame = endFrame;
			this.duration = duration;
		}

		/**
		 * Figures out how much time should be spent per frame so all the frames
		 * can be played within the duration.
		 * 
		 * @return The amount of time per frame in ms.
		 */
		public float timePerFrame() {
			return duration / (endFrame - startFrame);
		}

		public String getName() {
			return name;
		}

		/**
		 * Gets the duration in seconds that the animation takes before it
		 * loops.
		 * 
		 * @return The duration in seconds.
		 */
		public float getDuration() {
			return duration;
		}

	}
}
